package Square;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import Border.Border;
import Border.BorderType;
import Border.Direction;
import be.kuleuven.cs.som.annotate.*;

public abstract class DefaultSquare extends Square{

	/**
	 * 
	 * Creates a new square with a given temperature, humidity percentage and a number of border directions
	 * 
	 * @param 	temperature
	 * 			The temperature the new square will have.
	 * @param 	humidity
	 * 			The humidity percentage the new square will have.
	 * @effect 	This DefaultSquare is initialized as a new Square with default values 
	 * 			| super()
	 * @effect	The given temperature is set as the new temperature of this square
	 * 			| setTemperatureWithoutMerge(temperature)
	 * @effect 	The given humidity is set as the new humidity of this square.
	 * 			| setHumidityWithoutMerge(humidity)
	 * 			
	 */
	public DefaultSquare(double temperature, BigDecimal humidity) throws IllegalArgumentException {
		super();
		setTemperatureWithoutMerge(temperature);
		setHumidityWithoutMerge(humidity);
	}	

	/**
	 * Get the maximum temperature that this square can have.
	 * 
	 */
	/**
	 * A variable containing the minimum temperature for this square
	 */
	private double minTemperature = -200.0;
	
	/**
	 * A variable containing the maximum temperature for this square
	 */
	private double maxTemperature = 5000.0;
	
	/**
	 * Get the minimum temperature of the square
	 * 	
	 */
	@Basic
	public double getMinTemperature() 
	{
		return minTemperature;
	}
	
	/**
	 * Method that returns the maximum temperature of the square
	 */
	@Basic
	public double getMaxTemperature() 
	{
		return maxTemperature;
	}
	
	/**
	 * Sets the minimum temperature for this square
	 * 
	 * @param 	minTemperature
	 * 			The new minimum temperature value
	 * @throws	IllegalArgumentException
	 * 			If the current temperature falls out the new given bound
	 * 			| getTemperature() < minTemperature
	 * @throws	The given minimum temperature is larger then the current maximum temperature
	 * 			| getMaxTemperature() < minTemperature
	 * @post	The minimum temperature equals the given value
	 * 			| new.getMinTemperature() == minTemperature
	 */
	@Raw
	public void setMinTemperature(double minTemperature) throws IllegalArgumentException
	{
		if(getTemperature() < minTemperature)
			throw new IllegalArgumentException();
		if(getMaxTemperature() < minTemperature)
			throw new IllegalArgumentException();
		this.minTemperature = minTemperature;
	}
	
	/**
	 * Sets the maximum temperature for this square
	 * 
	 * @param 	maxTemperature
	 * 			The new maximum temperature value
	 * @throws	IllegalArgumentException
	 * 			If the current temperature falls out the new given bound
	 * 			| getTemperature() > maxTemperature
	 * @throws	The given maximum temperature is smaller then the current minimum temperature
	 * 			| getMinTemperature() > maxTemperature
	 * @post	The maximum temperature equals the given value
	 * 			| new.getMaxTemperature() == maxTemperature
	 */                       
	@Raw
	public void setMaxTemperature(double maxTemperature) throws IllegalArgumentException
	{
		if(getTemperature() > maxTemperature)
			throw new IllegalArgumentException();
		if(getMinTemperature() > maxTemperature)
			throw new IllegalArgumentException();
		
		this.maxTemperature = maxTemperature;
	}
	
	/**
	 * A variable containing the temperature of this square
	 */
	private double temperature;
	
	/**
	 * A variable containing the humidity of the Square.
	 */
	private BigDecimal humidity;
	
	
	/**
	 * Returns the temperature of this square
	 * 
	 */
	@Override @Basic
	public double getTemperature() {
		return this.temperature;
	}
	
	/**
	 * Returns the humidity of this RegularSquare
	 * 
	 */
	@Override @Basic
	public BigDecimal getHumidity() {
		return this.humidity;
	}
	/**
	 * Returns the constant value that will be used to calculate the average temperature
	 * for the mergeWith method
	 * 			
	 */
	@Basic
	public static double getMergeConstant()
	{
		return DefaultSquare.mergeConstant;
	}
	
	/**
	 * 
	 * Sets the constant that will be used to calculate the average temperature to a given value
	 * 
	 * @param 	value
	 * 			The new value for the constant
	 * @post	The constant that will be used to calculate the weighted mean will get the value of the given parameter
	 * 			| new.mergeConstant == value
	 * @throws 	IllegalArgumentException
	 * 			The given value isn't a value between 0.1 and 0.4
	 * 			| (value < 0.1) || (value value > 0.4)
	 */
	public static void setMergeConstant(double value) throws IllegalArgumentException
	{
		if(value >= 0.1 && value <= 0.4)
			DefaultSquare.mergeConstant = value;
		else
			throw new IllegalArgumentException();
	}
	
	/**
	 * Check whether the given temperature is a valid one.
	 * 
	 * @param   temperature
	 *          The temperature to check.
	 * @param	minTemperature
	 * 			The minimum temperature bound
	 * @param	maxTemperature
	 * 			The maximum temperature bound
	 * @return  True if and only if the given temperature is a value between the minimum and maximum
	 * 			temperature bounds
	 *          | result == (temperature >= minTemperature) && (temperature <= maxTemperature)
	 */
	public static boolean isValidTemperature(double temperature, double minTemperature, double maxTemperature) 
	{
		return (temperature >= minTemperature) && (temperature <= maxTemperature);
	}
	
	/**
	 * Set the squares temperature to the given number.
	 * 
	 * @param 	temperature
	 * 		  	The new temperature
	 * 
	 * @effect	The temperature is set to the given value
	 * 			| setTemperatureWithoutMerge( temperature )
	 * 
	 * @effect	The temperature for the squares in this square's space are recalculated
	 * 			| this.mergeSpace()
	 * 	
	 */
	public void setTemperature(double temperature) throws IllegalArgumentException
	{
		this.setTemperatureWithoutMerge(temperature);
		this.mergeSpace();
	}
	
	/**
	 * Set the squares temperature to the given number.
	 * 
	 * @param 	temperature
	 * 		  	The new temperature
	 * 
	 * @throws 	IllegalArgumentException
	 * 		   	The given temperature is not valid
	 * 		   	| !isValidTemperature(temperature, getMinTemperature(), getMaxTemperature())
	 * 
	 * @post 	The temperature of the square equals the given temperature
	 * 		 	| new.getTemperature() == temperature
	 * 
	 */
	@Raw		
	public void setTemperatureWithoutMerge(double temperature) throws IllegalArgumentException
	{
		if(!RegularSquare.isValidTemperature(temperature, getMinTemperature(), getMaxTemperature()))
			throw new IllegalArgumentException();
		else
		{
			this.temperature = temperature;
		}
	}
	
	/**
	 * 
	 * @param 	humidity
	 * 			The humidity percentage to check.
	 * 
	 * @return 	True if and only if the given humidity is a valid percentage (value between 0 and 100)
	 * 			and the precision of the given value can't be more then 2 digits nor a nullpointer.
	 * 			| result == (humidity >= 0) && (humidity <= 100) && humidity.precision() <= 2 && (humidity != null)
	 * 
	 */
	public static boolean isValidHumidity(BigDecimal humidity)
	{
		return (humidity.compareTo(new BigDecimal(0.00)) >= 0   ) 
			&& (humidity.compareTo(new BigDecimal(100.00)) <= 0)
			&& (humidity.precision() <= 2)
			&& (humidity != null);
	}
	
	/**
	 * 
	 * Sets the square's humidity to the given value
	 * 
	 * @param 	humidity
	 * 		  	| The new humidity percentage for the square
	 * 
	 * @effect	The humidity is set to the given value
	 * 			| setHumidityWithoutMerge(humidity)
	 * 
	 * @post	The temperature for the squares in this square's space are recalculated
	 * 			| this.mergeSpace()
	 * 
	 */
	@Raw
	public void setHumidity(BigDecimal humidity) 
	{
		setHumidityWithoutMerge(humidity);
		this.mergeSpace();
	}
	
	/**
	 * 
	 * Sets the square's humidity to the given value
	 * 
	 * @param 	humidity
	 * 		  	| The new humidity percentage for the square
	 * 
	 * @pre 	The given humidity has to be valid percentage.
	 * 			| isValidHumidity(humidity)
	 * 
	 * @post 	The humidity percentage of the square equals the given humidity 
	 * 	     	| new.getHumidity() == humidity
	 * 
	 * 
	 */
	@Raw
	public void setHumidityWithoutMerge(BigDecimal humidity) 
	{
		assert isValidHumidity(humidity);
		this.humidity = humidity;
	}
	
	
	/**
	 * A variable containing the constant that's being used in the mergeWith temperature calculation
	 */
	private static double mergeConstant = 0.1;	
	
	/**
	 * 
	 * Merge the square with another given square over a given border direction
	 * 
	 * @param 	other
	 * 			The other square to merge with
	 * @param 	direction
	 * 			The border direction to merge on
	 * @throws	IllegalArgumentException
	 * 			The given direction is not effective
	 * 			| direction == null 
	 * @throws 	IllegalArgumentException
	 * 			The square that's trying to be merged with is the object itself
	 * 			| other == this
	 * @throws 	IllegalArgumentException
	 * 			The given square is not effective
	 * 			| other == null
	 * @effect	The humidity of the squares will be set to the average of the two
	 * 			| newHumidity = (this.getHumidity() + other.getHumidity()) / 2;
	 * 			| this.setHumidity(newHumidity)
	 * 			| other.setHumidity(newHumidity)
	 * @effect	The borders at the given direction will be merged
	 * 			| this.mergeBorders(other, direction)
	 * @effect	The temperature will be set to the average temperature based on the thermal capacity of water
	 * 			for both squares
	 * 			| averageTemperature = calculateAverageTemperature(this, other);
	 *			| this.setTemperature( averageTemperature );
	 *			| other.setTemperature( averageTemperature );
	 *
	 */
	public void mergeWith(DefaultSquare other, Direction direction) throws IllegalArgumentException
	{
		
		if(this == other || this == null || direction == null)
			throw new IllegalArgumentException();
		
		this.mergeBorders(other, direction);
		
		BigDecimal averageHumdity = this.getHumidity().add(other.getHumidity()).divide( new BigDecimal(2.0));
			
		
		double averageTemperature = calculateAverageTemperature(this, other);
		
		this.setTemperature( averageTemperature );
		other.setTemperature( averageTemperature );
		
		this.setHumidity(averageHumdity);
		other.setHumidity(averageHumdity);
	}
	
	/**
	 * 
	 * Calculate the average temperature from 2 given squares
	 * 
	 * @param 	squareOne
	 * 			The first square.
	 * @param 	squareTwo
	 * 			The second square.
	 * @return	The average temperature based on the thermal capacity of water
	 * 			| averageHumdity = (squareOne.getHumidity() + squareTwo.getHumidity()) / 2;
	 *			| a = 1 - getMergeConstant();
	 *			| if(averageHumdity == 0) then
	 *			| 	weightedMeanOne = 1, weightedMeanTwo = 1
	 *			| else
	 *			| 	weightedMeanOne = ( (a*squareOne.getHumidity()) / averageHumdity ) + getMergeConstant();
	 *			| 	weightedMeanTwo = ( (a*squareTwo.getHumidity()) / averageHumdity ) + getMergeConstant();
	 *			|
	 *			| result == ((weightedMeanOne * squareOne.getTemperature()) + (weightedMeanTwo * squareTwo.getTemperature())) / 2;
	 */
	private static double calculateAverageTemperature(Square squareOne, Square squareTwo)
	{
		BigDecimal averageHumdity = squareOne.getHumidity().add(squareTwo.getHumidity()).divide( new BigDecimal(2.0));
		double a = 1 - getMergeConstant();
		
		double weightedMeanOne, weightedMeanTwo;
	
		if(averageHumdity.compareTo(new BigDecimal(0)) == 0)
		{
			weightedMeanOne = 1;
			weightedMeanTwo = 1;
		}
		else
		{
			
			weightedMeanOne = squareOne.getHumidity().multiply(new BigDecimal(a)).divide(averageHumdity).add(new BigDecimal(getMergeConstant())).doubleValue();
			weightedMeanTwo = squareTwo.getHumidity().multiply(new BigDecimal(a)).divide(averageHumdity).add(new BigDecimal(getMergeConstant())).doubleValue();
		}
		double averageTemperature = ((weightedMeanOne * squareOne.getTemperature()) + (weightedMeanTwo * squareTwo.getTemperature())) / 2;
		return averageTemperature;
	}
	
	/**
	 * Method that returns a array with all the squares that are linked to the given square.
	 * 
	 * @param		startingSquare
	 * 				Parameter that resembles the startingSquare at which the check is started
	 * 
	 * @return		Adds all squares that are linked to this square to an arraylist and returns this list
	 * 				| result == allLinkedSquares
	 */
	public ArrayList<DefaultSquare> getAllLinkedSquares(){
		ArrayList<DefaultSquare> allLinkedSquares = new ArrayList<DefaultSquare>();
		getSquaresFromAllDirections(this, allLinkedSquares);
		return allLinkedSquares;
	}
	
	/**
	 * Method that puts all the Squares, linked to the given Square, into a arraylist.
	 * These squares are linked to eachother when they are right to eachother with a open door or empty border between them.
	 * 
	 * @param 		startingSquare
	 * 				Parameter that resembles the start of the check.
	 * 
	 * @param 		allLinkedSquares
	 * 				The ArrayList which contains all the squares that are linked to the given square.
	 * 
	 * @post 		To make sure we don't add any more squares that are already in the ArrayList, we check
	 * 				if the square is already in the ArrayList.
	 * 				| if ( allLinkedSquares.contains(startingSquare) == false)
	 *				|		allLinkedSquares.add(startingSquare);
	 * 
	 * @post		Recursively calls this method again for all the startingSquare's surrounding squares
	 * 				| for each direction in Border.Direction
	 *				|	otherSquare = startingSquare.getBorderAt(direction).getOtherSquare(startingSquare);
	 *				|	border = startingSquare.getBorderAt(direction);
	 *				|	if ( border.getBorderType() == Border.BorderType.EMPTY || border.getBorderType() == Border.BorderType.OPEN_DOOR) 
	 *				|		if ( otherSquare != null && !allLinkedSquares.contains(otherSquare))
	 *				|			getSquaresFromAllDirections(otherSquare, allLinkedSquares);
	 *
	 * @note		Time complexity: O( 5^n )
	 */
	private ArrayList<DefaultSquare> getSquaresFromAllDirections(DefaultSquare startingSquare, ArrayList<DefaultSquare> allLinkedSquares){
		if ( allLinkedSquares.contains(startingSquare) == false)
			allLinkedSquares.add(startingSquare);
		
		for(Direction direction : Direction.values())
		{
			DefaultSquare otherSquare = (DefaultSquare) startingSquare.getBorderAt(direction).getOtherSquare(startingSquare);
			Border border = startingSquare.getBorderAt(direction);
			if ( border.getBorderType() == BorderType.EMPTY || border.getBorderType() == BorderType.OPEN_DOOR) 
				if ( otherSquare != null && !allLinkedSquares.contains(otherSquare))
					allLinkedSquares = getSquaresFromAllDirections(otherSquare, allLinkedSquares);
		}	
		return allLinkedSquares;

	}	
	

	/**
	 *	A method that merges all squares in the same space as the parent square
	 *
	 *	@post	All the squares in the room's temperature and humdity will be set to the average values
	 *			| totalTemperature = 0
	 *			| totalHumdity = 0
	 *			| for each square in this.getAllLinkedSquares()
	 *			| 	totalTemperature += square.getTemperature() 
	 *			|   totalHumdity += square.getHumidity()
	 *			| for each square in this.getAllLinkedSquares()
	 *			| 	new.getTemperature == totalTemperature / this.getAllLinkedSquares().size() )
	 *			|   new.getHumidity == totalHumdity / this.getAllLinkedSquares().size() 
	 */
	public void mergeSpace( )
	{
		ArrayList<DefaultSquare> room = this.getAllLinkedSquares();
		BigDecimal totalHumidity = new BigDecimal(0.0);
		double totalTemperature = 0.0;
		for(int i = 0; i < room.size(); i++)
		{
			totalHumidity = totalHumidity.add( room.get(i).getHumidity());
			totalTemperature += room.get(i).getTemperature();
		}
		
		BigDecimal averageHumidity = totalHumidity.divide( new BigDecimal( room.size()   ), 2, BigDecimal.ROUND_HALF_UP );
		
		double averageTemperature = (double)totalTemperature / room.size();
		
		for(int i = 0; i < room.size(); i++)
		{
			((DefaultSquare) room.get(i)).setTemperatureWithoutMerge(averageTemperature);
			((DefaultSquare) room.get(i)).setHumidityWithoutMerge(averageHumidity);
		}
	}
	

	/**
	 * Method that returns a list of defaultSquares, this list represents all the squares that are reachable
	 * by the given square
	 * 
	 * 
	 * @return		Returns all the reachable squares
	 * 				 | currentSquares = new ArrayList<DefaultSquare>();
	 *	             | currentSquares.add(this);
	 * 	             | result == getAllReachableSquaresByTeleportation(currentSquares,this);
	 * 
	 */
	public List<DefaultSquare> getAllReachableSquares() {
		List<DefaultSquare> currentSquares = new ArrayList<DefaultSquare>();
		currentSquares.add(this);
		currentSquares = getAllReachableSquaresByTeleportation(currentSquares,this);
		return currentSquares;
	}
	
	/**
	 * Recursive method that adjusts the List currentSquares.
	 * 
	 * @param 		newList
	 * 				List that is used to keep track of all the squares that the given square can reach
	 * 					
	 * @param 		square
	 * 				The square where a teleporter is able to teleport to.
	 *
	 * @return		Returns newList + all squares reachable from square added
	 * 				| reachableSquares = square.getAllLinkedSquares()
	 *				| if(square.isTeleportationSquare())
	 *				|	reachableSquares.addAll( square.getAllDestinations())
	 *				| for each defSquare in reachableSquares){
	 *				|	if ( ! newList.contains(defSquare)){
	 *				|		newList.add(defSquare)
	 *				|		newList = getAllReachableSquaresByTeleportation(newList,defSquare)					
	 *				|	}
	 *				| }
	 *				| result == newList
	 * 							 
	 */
	private List<DefaultSquare> getAllReachableSquaresByTeleportation(List<DefaultSquare> newList, DefaultSquare square){
		List<DefaultSquare> reachableSquares = square.getAllLinkedSquares();
	
		if(square.isTeleportationSquare()){
			reachableSquares.addAll( ((Teleporter) square).getAllDestinations());
		}
		for(DefaultSquare defSquare : reachableSquares){
			if ( ! newList.contains(defSquare)){
				newList.add(defSquare);
				newList = getAllReachableSquaresByTeleportation(newList,defSquare);					
			}
		}
		
		return newList;
	}
	
	/**
	 * Method that checks if the given square can be reached by teleportation or just room
	 * movement
	 * 
	 * @param		square
	 * 				| Square that is going to be checked if it is reachable
	 * 
	 * @return		Returns true if the square is reachable and false if not
	 * 				| result == this.getAllReachableSquares().contains(square)
	 */
	public boolean canReachSquare(DefaultSquare square){
		return this.getAllReachableSquares().contains(square);
	}
	/**
	 * Method that checks if the square is a teleporter or not.
	 * 
	 * @return		Returns true if the square is a teleporter and false if not.
	 * 				| result == (this instanceof Teleporter)
	 */
	public boolean isTeleportationSquare()
	{
		return this instanceof Teleporter;
	}
	
}
